<?php if (!defined('CORE_DIR')) die('Wrong entry point!');

/* 
 * Copyright 2010 Anton Muraviev a.k.a kuromaisu
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */


// =============================================================================
// Section: Code Inspector
// =============================================================================
//

Aero::define('AeroCodeInspector', AS_CLASS);

/* -----------------------------------------------------------------------------
 * Class: AeroCodeInspector
 * -----------------------------------------------------------------------------
 *
 * A proper PHP code parser which is using token_get_all()... it is supposed to be.
 * But it is really just a crude hack on top of highlight_*(). Specification lib 
 * is using this mess to highlight proper function.
 * 
 */
class AeroCodeInspector
{
	protected $lines = array();
	
	/*
	 * Constructor: __construct
	 * 
	 * Parameters:
	 * 	string	-	file path or code listing
	 * 	bool	-	(optional) whether to interpret the first parameter as file path
	 */
	public function __construct($file_or_string, $load = true)
	{
		if ($load === true)
		{
			$source = highlight_file($file_or_string,true);
		}
		else
		{
			$source = highlight_string("<?php \n".$file_or_string."\n?>",true);
		}
		
		// First, replace all span elements with code elements.
		$code_classes = array(
			'string' => 'php-string',
			'comment' => 'php-comment',
			'keyword' => 'php-keyword',
			'default' => 'php-code',
			'html' => 'html'
		);
		$find = array('<code>','</code>','</span>','&nbsp;');
		$replace = array('','','</code>',' ');
		
		foreach ($code_classes as $type => $class)
		{
			$find[] = '<span style="color: '.ini_get('highlight.'.$type).'">';
			$replace[] = '<code class="'.$class.'">';
		}
		
		$source = str_replace($find,$replace,$source);
		
		// Now remove the outer "html" wrapper. 
		// TODO: find a better solution.
		$source = substr($source, strlen('<code class="html">'), strlen($source) - strlen('</code>') - strlen('<code class="html">') - 1);
		
		// Remove <br> from code element. This will prevent code elements spanning multiple lines.
		$source = preg_replace_callback('~<code class="(.*?)">(.*?)</code>~', array($this,'_removedNestedBreaks'), $source);
		
		$source = str_replace(array("\n","\r"), '', $source);
		
		// Break the source into lines.
		$this->lines = explode('<br />',$source);
		
		if ($load === false)
		{
			$this->lines = array_slice($this->lines, 1, count($this->lines) - 2);
		}
	}
	
	/*
	 * Method: getLine
	 * 
	 * Returns specified line
	 * 
	 * Parameters:
	 * 	int	-	line number (1-based)
	 * 
	 * Returns:
	 * 	string
	 */
	public function getLine($line)
	{
		return $this->lines[$line - 1];
	}
	
	/*
	 * Method: getLines
	 * 
	 * Returns specified lines
	 * 
	 * Parameters:
	 * 	int		-	first line number (1-based)
	 * 	int		-	last line number (1-based)
	 * 	bool	-	whether to trim whitespace
	 * 
	 * Returns:
	 * 	array
	 */
	public function getLines($first = 0, $last = 0, $whitespace = false)
	{
		if ($first === 0 && $last === 0)
		{
			$lines = $this->lines;
		}
		else if ($last === 0)
		{
			$lines = array_slice($this->lines, $first - 1);
		}
		else
		{
			$lines = array_slice($this->lines, $first - 1, $last - $first + 1);
		}
		
		if (count($lines) == 0)
		{
			return array();
		}
		
		if (!$whitespace)
		{
			$this->_trimWhitespace($lines);
		}
		
		$line_nums = range($first, $first + count($lines) - 1, 1);
		
		return array_combine($line_nums, $lines);
	}
	
	/*
	 * Method: findLine
	 * 
	 * Finds the first line that matches criteria withing the span.
	 * 
	 * Parameters:
	 * 	array	-	array of criteria
	 * 	int		-	first line number (1-based)
	 * 	int		-	last line number (1-based)
	 * 
	 * Returns:
	 * 	int
	 */
	public function findLine($criteria, $first_line = 0, $last_line = 0)
	{
		return $this->_find($criteria, $first_line, $last_line);
	}
	
	/*
	 * Method: findLineReverse
	 * 
	 * Finds the first line that matches criteria withing the span in reverse order.
	 * 
	 * Parameters:
	 * 	array	-	array of criteria
	 * 	int		-	first line number (1-based)
	 * 	int		-	last line number (1-based)
	 * 
	 * Returns:
	 * 	int
	 */
	public function findLineReverse($criteria, $first_line = 0, $last_line = 0)
	{
		return $this->_find($criteria, $first_line, $last_line, true);
	}
	
	/*
	 * Method: _find
	 */
	protected function _find($criteria, $first_line = 0, $last_line = 0, $reverse = false, $multiline = false)
	{
		if (!is_array($criteria))
		{
			$criteria = array($criteria);
		}
		
		if ($reverse)
		{
			$criteria = array_reverse($criteria,true);
		}
		
		$total = count($this->lines);
		
		$start = $first_line > 0 ? min($first_line,$total,$last_line) : 1;
		$end = $last_line > 0 ? min($last_line,$total) : $total;
		
		$line = $reverse ? $end + 1 : $start - 1;
		
		while (($line < $end && !$reverse) || ($line > $start && $reverse))
		{
			$reverse ? $line-- : $line++;
			$source = $this->lines[$line - 1];
			
			preg_match_all('~<code class="php-(\w+)">(.*?)</code>~',$source,$blocks,PREG_SET_ORDER);
			
			if ($reverse)
			{
				$blocks = array_reverse($blocks,true);
			}
			
			$sequence = false;
			
			foreach ($criteria as $type => $value)
			{
				if ($value === '*')
				{
					$sequence = false;
					continue;
				}
				
				while ($current = array_shift($blocks))
				{
					$_type = $current[1];
					$_value = $current[2];
					
					if ((!is_string($type) || $type === '*' || $type === $_type)
						&& strpos($_value, $value) !== false)
					{
						$sequence = true;
						
						continue 2; // next criteria
					}
					else if ($sequence)
					{
						continue 3; // try next line
					}
				}
				
				continue 2;
			}
			
			return $line;
		}
		
		return false;
	}
	
	/*
	 * Method: _removedNestedBreaks
	 */
	protected function _removedNestedBreaks($matches)
	{
		if (strpos($matches[2], '<br />') === false)
		{
			return $matches[0];
		}
		else
		{
			$source = explode('<br />', $matches[2]);
			
			return '<code class="'.$matches[1].'">'.implode('</code><br /><code class="'.$matches[1].'">',$source).'</code>';
		}
	}
	
	/*
	 * Method: _trimWhitespace
	 */
	protected function _trimWhitespace(&$lines)
	{
		$whitespace = null;
		
		foreach ($lines as $line)
		{
			$stripped = strip_tags($line);
			
			if (preg_match('~^<code class="php-(\w+)">(\s*)~',$line,$matches) === 0)
			{
				return;
			};
			
			if (strlen($stripped) > 0 && (is_null($whitespace) || strlen($whitespace) > strlen($matches[2])))
			{
				$whitespace = $matches[2];
			}
		}
		
		$lines = preg_replace('~^(<code class="php-\w+">)'.$whitespace.'~', '$1', $lines);
	}
}
